package touchtrack.view.graph;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import org.mt4j.MTApplication;
import org.mt4j.components.MTComponent;

import touchtrack.controller.ArtistController;
import touchtrack.controller.TrackController;
import touchtrack.exception.ItemNotFoundException;
import touchtrack.model.track.TrackInterface;
import touchtrack.util.TrackLoader;
import traer.physics.Attraction;
import traer.physics.Particle;
import traer.physics.ParticleSystem;

/**
 * Graph
 * 
 * @author	MuMe09 team 6
 * 			http://mumetech.wordpress.com/
 */
public class TrackGraph extends MTComponent
{
	private MTApplication app;
	
	/**
	 * Graph constructor.
	 * 
	 * @param 	MTApplication
	 * @param	ArtistController
	 * @param 	TrackController
	 */
	public TrackGraph(MTApplication app, ArtistController artistController, TrackController trackController)
	{
		super(app);
		this.app = app;
		this.artistController = artistController;
		this.trackController = trackController;
		this.trackLoader = new TrackLoader(artistController, trackController);
		
		createGraph();
	}

	/***************************************************************************
	 * 							  Drawing
	 ***************************************************************************/
	
	/**
	 * Draws nodes & springs.
	 */
	public void draw() 
	{
		++frame;
		app.background(0);
		
		if(!paused)
		{			
			for (TrackSpring s : springs) 
			    s.draw(this);
			
			for (TrackNode t : nodes)
				t.draw(this);
			
			doPhysics();
		}
	}
	
	private long frame = 0;
	public boolean paused = false;
    public final int BORDER = 160;

	/***************************************************************************
	 * 							  Graph coordinates
	 ***************************************************************************/
	
    private int minX = Integer.MAX_VALUE;
    private int minY = Integer.MAX_VALUE;
    private int maxX = Integer.MIN_VALUE;
    private int maxY = Integer.MIN_VALUE;

    /**
     * Updates min/max x/y coordinates.
     */
    private void updateMinMaxXY()
    {
        minX = Integer.MAX_VALUE;
        minY = Integer.MAX_VALUE;
        maxX = Integer.MIN_VALUE;
        maxY = Integer.MIN_VALUE;
        
        for (TrackNode n : nodes)
        {
            Particle p = n.particle;
            if (p != null)
            {
                if (p.position().x() < minX) {
                    minX = (int) p.position().x();
                }
                if (p.position().x() > maxX) {
                    maxX = (int) p.position().x();
                }
                if (p.position().y() < minY) {
                    minY = (int) p.position().y();
                }
                if (p.position().y() > maxY) {
                    maxY = (int) p.position().y();
                }
            }
        }
    }

    public int getMaxX()
    {
        return maxX;
    }

    public int getMaxY()
    {
        return maxY;
    }

    public int getMinX()
    {
        return minX;
    }

    public int getMinY()
    {
        return minY;
    }

	/***************************************************************************
	 * 							  Creation
	 ***************************************************************************/
	
	/**
	 * Creates track nodes & track spring objects.
	 */
	public void createGraph()
	{
		// Flush lists
		this.nodes = new LinkedList<TrackNode>();
		this.springs = new LinkedList<TrackSpring>();
		
		// Get list of all tracks.
		LinkedList<TrackInterface> tis = (LinkedList<TrackInterface>) trackController.getTracks();
		
		// Generate track image objects.
		for ( TrackInterface t : tis )
		{
			TrackNode node1 = addTrackNode(t);
			
			// Add springs.
			for ( TrackInterface similar : t.getSimilarTracks() )
			{
				TrackNode node2 = addTrackNode(similar);
				addSpring(node1, node2);
			}
		}
	}

	/***************************************************************************
	 * 							  	Nodes
	 ***************************************************************************/
	
	/**
	 * @param 	trackInterface
	 * @return	TrackNode	(@todo vind elegantere manier)
	 */
	public TrackNode addTrackNode(TrackInterface trackInterface)
	{
		TrackNode trackNode = new TrackNode(app, trackInterface);
		
		if (!nodes.contains(trackNode))
		{
			nodes.add(trackNode);
			showNode(trackNode);
			System.out.println("[DEBUG] Added node " + trackNode);
		}
		else
		{
			for (TrackNode n : nodes)
				if (n.equals(trackNode))
					trackNode = n;
		}
		
		return trackNode;
	}
	
	/**
	 * Returns a list of neighbor nodes.
	 * 
	 * @param 	TrackNode node
	 * @return	TrackNode
	 */
	public List<TrackNode> getNeighbors(TrackNode node)
	{
		LinkedList<TrackNode> neighbors = new LinkedList<TrackNode>();
		
		for ( TrackSpring spring : springs )
		{
			try {
				neighbors.add(spring.getOtherNode(node));
			} catch (ItemNotFoundException e) {
				// skip
			}
		}
		
		return neighbors;
	}
	
	/**
	 * @param 	TrackNode node
	 */
	public void removeNode(TrackNode node)
	{
		paused = true;
		node.remove(physics);
		try {
			trackController.removeTrack(node.toString());
		} catch (ItemNotFoundException e) {
			// vreemd
			e.printStackTrace();
		}
		this.createGraph();
		paused = false;
	}
	
	/**
	 * @param 	float x
	 * @param 	float y
	 * @return	TrackNode
	 */
	public TrackNode searchNode(float x, float y)
	{
		for (TrackNode node : nodes)
		{
			if (node.inImage(this, x, y))
				return node;
		}
		
		return null;
	}
	
	/**
	 * @param 	float x
	 * @param 	float y
	 * @return	TrackNode
	 */
	public TrackNode searchInfoButton(float x, float y)
	{
		for (TrackNode node : nodes)
		{
			if (node.inInfoButton(this, x, y))
				return node;
		}
		
		return null;
	}
	
	/**
	 * @param 	float x
	 * @param 	float y
	 * @return	TrackNode
	 */
	public TrackNode searchRemoveButton(float x, float y)
	{
		for (TrackNode node : nodes)
		{
			if (node.inRemoveButton(this, x, y))
				return node;
		}
		
		return null;
	}
	
	public LinkedList<TrackNode> nodes;

	/***************************************************************************
	 * 							  	Springs
	 ***************************************************************************/
	
	/**
	 * 
	 * @param 	TrackNode
	 * @param 	TrackNode
	 * @return	Spring	(@todo vind elegantere manier)
	 */
	public TrackSpring addSpring(TrackNode track1, TrackNode track2)
	{
		TrackSpring spring = new TrackSpring(app, track1, track2);
		
		if (!springs.contains(spring))
		{
			springs.add(spring);
			showSpring(spring);
			System.out.println("[DEBUG] Added spring " + spring);
		}
		else
		{
			for (TrackSpring s : springs)
				if (s.equals(spring))
					spring = s;			
		}
		
		return spring;
	}
	
	/**
	 * Checks whether there's a Spring between the two given track nodes 
	 * and returns it if possible.
	 * Otherwise: exception!
	 * 
	 * @param 	TrackNode track1
	 * @param 	TrackNode track2
	 * @return	Spring
	 * @throws 	ItemNotFoundException
	 * 			Thrown when there doesn't exist a Spring between the tracks.
	 */
	public TrackSpring findSpring(TrackNode track1, TrackNode track2) 
		throws ItemNotFoundException
	{
		for (TrackSpring s : springs)
		{
			if (s.between(track1, track2))
				return s;
		}
		
		throw new ItemNotFoundException("A spring between " + track1 + " and " + track2);
	}
	
	public LinkedList<TrackSpring> springs;	

	/***************************************************************************
	 * 							  	Physics
	 ***************************************************************************/
	
	/**
	 * Uses force-based algorithms to calculate good positions for the nodes and edges.
	 */
	private void doPhysics()
	{
		physics.tick(0.1F);
        updateMinMaxXY();
	}

	/**
	 * @param 	TrackSpring
	 */
    private void showSpring(TrackSpring trackSpring)
    {
        Particle p1 = trackSpring.track1.particle;
        Particle p2 = trackSpring.track2.particle;
        
        if (p1 == null || p2 == null)
        	return;        	
       
        for (int i = 0; i < physics.numberOfAttractions(); ++i)
        {
            Attraction a = physics.getAttraction(i);
            if ((a.getOneEnd() == p1 && a.getTheOtherEnd() == p2) 
            	|| (a.getTheOtherEnd() == p1 && a.getOneEnd() == p2))
                	a.turnOff();
        }
        
        trackSpring.spring = physics.makeSpring(p1, p2, SPRING_STRENGTH, SPRING_STRENGTH, 
        		SPRING_LENGTH - (int) (trackSpring.getStrength()));
    }

    /**
     * @param 	TrackNode
     */
    private void showNode(TrackNode trackNode)
    {
        // node shouldn't have a particle, but if it does, kill it
        if (trackNode.particle != null)
        	trackNode.killParticle(physics);
        
        trackNode.particle = physics.makeParticle();
        
        float x1 = 0.0F;
        float y1 = 0.0F;
        int pCount = 0;
        
        // create a list of particles that should be connected to
        // our newly visible node
        // cps -- ConnectedParticles
        HashMap<Particle, Integer> cps = new HashMap<Particle, Integer>();
        for (TrackSpring ts : springs)
        {
            if (ts.track1.equals(trackNode) && ts.track2.particle != null) 
            {
                cps.put(ts.track2.particle, (int) ts.getStrength());
                x1 += ts.track2.particle.position().x();
                y1 += ts.track2.particle.position().y();
                pCount++;
            }
            
            if (ts.track2.equals(trackNode) && ts.track1.particle != null)
            {
                cps.put(ts.track1.particle, (int) ts.getStrength());
                x1 += ts.track1.particle.position().x();
                y1 += ts.track1.particle.position().y();
                pCount++;
            }
        }
        
        if (pCount > 0)
        {
            x1 = (float) ((float) (x1 / pCount) + (25 * (Math.random() - 0.5)));
            y1 = (float) ((float) (y1 / pCount) + (25 * (Math.random() - 0.5)));
        } 
        else
        {
            x1 = (float) ((float) 200.0F + (25 * (Math.random() - 0.5)));
            y1 = (float) ((float) 150.0F + (25 * (Math.random() - 0.5)));
        }
        
        //System.out.println("x = " + x1 + "\ty = " + y1);
        trackNode.moveParticle(x1, y1);
        
        // run through every particle and create either a spring
        // when there is an edge, or a negative attraction where
        // there isn't
        for (int i = 0; i < physics.numberOfParticles(); ++i)
        {
            Particle op = physics.getParticle(i);
            if (op != trackNode.particle)
            {
                if (cps.containsKey(op))
                {
                    physics.makeSpring(trackNode.particle, op, SPRING_STRENGTH, SPRING_STRENGTH, SPRING_LENGTH - cps.get(op));
                }
                else
                {
                    physics.makeAttraction(trackNode.particle, op, -SPACER_STRENGTH, 80);
                }
            }
        }
    }
	
    private ParticleSystem physics = new ParticleSystem(0, 0.25F);
    public static final float SPRING_LENGTH = 140;
    public static final float SPRING_STRENGTH = 0.4F;
    public static final float SPACER_STRENGTH = 2000;
    public static final float MIN_MATCH_SCORE = 30.0F;
	
	/***************************************************************************
	 * 								Controllers (& loader)
	 ***************************************************************************/
    
    /**
     * @return	TrackLoader
     */
    public TrackLoader getTrackLoader()
    {
    	return this.trackLoader;
    }
    
	private TrackLoader trackLoader;
	private ArtistController artistController;
	private TrackController trackController;

	/***************************************************************************
	 * 								Java stuff
	 ***************************************************************************/
	
	private static final long serialVersionUID = 1L;
}